Skip to content

Linux 管道 IPC:匿名管道与有名管道 ​

管道是 UNIX 系统最古老的进程间通信机制:ls | wc -l 里的 | 就是一根管道。本文写给写 Linux 多进程程序的开发者,讲清两类管道的区别与各自的正确用法——匿名管道(pipe)为什么只能给亲缘进程用、有名管道(FIFO)如何让任意进程通信、读写端各种关闭组合下会发生什么。读完能避开管道使用中最常见的几个坑。

进程间通信的方式 ​

进程是独立的资源分配单元,不同进程之间不能直接访问对方的资源,需要内核提供通信机制。Linux 的 IPC 手段按来源分三类:

消息队列、共享内存、信号量同时有 System V 与 POSIX 两套接口(共享内存与内存映射见 Linux 共享内存与内存映射,异步通知见 Linux 信号机制:产生、阻塞与捕捉)。

匿名管道的特点 ​

  • 管道是内核内存中维护的缓冲区,容量有限,不同操作系统大小不一定相同
  • 管道拥有文件的特质(读、写操作),匿名管道没有文件实体,有名管道有文件实体但不存储数据
  • 管道是一个字节流:不存在消息边界,读取方每次读多少字节都合法,写入的数据块可能被合并
  • 数据严格按写入顺序读出,读走即消失,无法用 lseek() 随机访问
  • 数据单向流动,一端读一端写,管道是半双工的
  • 匿名管道只能用于有公共祖先的进程(父子、兄弟进程)——它靠 fork 时描述符的继承传递,没有名字可供无关进程打开

一个容易混淆的概念:fpathconf(fd, _PC_PIPE_BUF) 查询的 PIPE_BUF 是单次写入保证原子的字节上限(Linux 通常为 4096),不是管道的总容量;Linux 上总容量用 fcntl(fd, F_GETPIPE_SZ) 查询,自内核 2.6.11 起默认为 65536 字节。

基本用法 ​

c
#include <unistd.h>
int pipe(int pipefd[2]);
// pipefd[0] 是读端,pipefd[1] 是写端;成功返回 0,失败返回 -1

pipe() 在 fork 之前调用,父子进程于是持有同一根管道的两端。习惯上各自关闭不用的那一端(父关写端、子关读端),这一点不只是省资源,直接决定了后面的读写行为:

c
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    int pipefd[2];
    int ret = pipe(pipefd);           // 先建管道
    if (ret == -1) {
        perror("pipe");
        exit(0);
    }

    pid_t pid = fork();               // 再 fork
    if (pid > 0) {
        // 父进程:关闭写端,只读
        close(pipefd[1]);
        char buf[1024] = {0};
        while (1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            if (len == 0) {           // 写端已全部关闭且数据读完:读到 EOF
                break;
            }
            printf("parent recv: %s\n", buf);
        }
    } else if (pid == 0) {
        // 子进程:关闭读端,只写
        close(pipefd[0]);
        char *str = "hello, i am child";
        while (1) {
            write(pipefd[1], str, strlen(str));
            sleep(1);
        }
    }
    return 0;
}

管道的读写规则 ​

阻塞模式下,读端与写端的关闭状态组合出四种行为,这是管道使用中最需要记住的一张表:

场景 行为
写端全部关闭,读端读 剩余数据读完后 read 返回 0(类似读到文件末尾)
写端还有进程持有,但不写数据 数据读完后 read 阻塞,直到有新数据
读端全部关闭,写端写 写进程收到 SIGPIPE 信号,默认终止进程
读端还有进程持有,但不读数据 管道写满后 write 阻塞,直到腾出空间

两个实践推论:

  • 判断「对方是否还活着」看 read 是否返回 0;写数据前如果不确定读端存在,要么装 SIGPIPE 处理器,要么用 send/带 MSG_NOSIGNAL 的接口替代场景,避免进程被信号杀掉。
  • 读到 EOF(返回 0)后应该退出读循环,继续 read 只会无限返回 0 造成忙等。

非阻塞模式 ​

对读端描述符设置 O_NONBLOCK,read 在无数据时立即返回 -1 并置 errno 为 EAGAIN,而不是挂起:

c
int flags = fcntl(pipefd[0], F_GETFL);
flags |= O_NONBLOCK;
fcntl(pipefd[0], F_SETFL, flags);

有名管道(FIFO) ​

匿名管道没有名字,无法被无关进程打开。有名管道(FIFO)在文件系统中创建一个特殊文件,任何能访问该路径的进程都可以打开它通信:

c
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
// 成功返回 0,失败返回 -1;也可用命令 mkfifo(1) 创建

FIFO 与匿名管道的 I/O 语义完全一致(字节流、半双工、同样的读写规则),区别只在两点:它在文件系统中有名字、有实体,进程退出后 FIFO 文件仍在;因此没有亲缘关系的进程也能通过路径找到同一根管道。

FIFO 的打开阻塞

FIFO 必须两端都打开才能传数据。默认(阻塞模式)下,open(O_RDONLY) 会阻塞到有进程以写方式打开,open(O_WRONLY) 反之亦然。 设置 O_NONBLOCK 后行为不对称:只读打开立即成功(即使尚无写端),只写打开在尚无读端时失败并返回 ENXIO。

写进程与读进程的完整示例:

c
// 写进程
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main() {
    // 不存在则创建
    int ret = access("test", F_OK);
    if (ret == -1) {
        ret = mkfifo("test", 0664);
        if (ret == -1) {
            perror("mkfifo");
            exit(0);
        }
    }

    int fd = open("test", O_WRONLY);   // 阻塞至读端打开
    if (fd == -1) {
        perror("open");
        exit(0);
    }

    for (int i = 0; i < 100; i++) {
        char buf[1024];
        sprintf(buf, "hello, %d\n", i);
        write(fd, buf, strlen(buf));
        sleep(1);
    }
    close(fd);
    return 0;
}
c
// 读进程
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = open("test", O_RDONLY);   // 阻塞至写端打开
    if (fd == -1) {
        perror("open");
        exit(0);
    }

    while (1) {
        char buf[1024] = {0};
        int len = read(fd, buf, sizeof(buf));
        if (len == 0) {                // 写端全部关闭:EOF
            printf("writer closed\n");
            break;
        }
        printf("recv: %s", buf);
    }
    close(fd);
    return 0;
}

用两条 FIFO 实现双工聊天 ​

单根管道是单向的,双向聊天需要两根 FIFO,各进程再用一对父子或线程分别负责读与写:

客户端代码(另一端只需互换两条 FIFO 的读写方向):

c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main() {
    // 两条管道不存在则创建
    if (access("fifo1", F_OK) == -1 && mkfifo("fifo1", 0664) == -1) {
        perror("mkfifo");
        exit(0);
    }
    if (access("fifo2", F_OK) == -1 && mkfifo("fifo2", 0664) == -1) {
        perror("mkfifo");
        exit(0);
    }

    int fdw = open("fifo1", O_WRONLY);
    int fdr = open("fifo2", O_RDONLY);
    if (fdw == -1 || fdr == -1) {
        perror("open");
        exit(0);
    }

    pid_t pid = fork();
    char buf[128];
    if (pid > 0) {
        // 父进程:标准输入 → fifo1
        while (1) {
            memset(buf, 0, 128);
            fgets(buf, 128, stdin);
            if (write(fdw, buf, strlen(buf)) == -1) {
                perror("write");
                break;
            }
        }
    } else if (pid == 0) {
        // 子进程:fifo2 → 标准输出
        while (1) {
            memset(buf, 0, 128);
            int ret = read(fdr, buf, 128);
            if (ret <= 0) {
                break;
            }
            printf("recv: %s", buf);
        }
    }

    close(fdw);
    close(fdr);
    return 0;
}

要点 ​

  • 匿名管道靠 fork 继承描述符,只适用于亲缘进程;FIFO 有路径名,任意进程可用
  • 管道是半双工字节流,读走即消失;PIPE_BUF 是原子写上限而非容量,容量用 fcntl(F_GETPIPE_SZ) 查
  • 四条读写规则:写端全关读返回 0,读端全关写触发 SIGPIPE,对端持有但不动作则阻塞
  • FIFO 打开会阻塞到对端打开,O_NONBLOCK 下只读与只写的行为不对称
  • 双向通信用两根管道,单根管道内不要试图双向传数据

相关文章 ​

最近更新

基于 VitePress 构建